﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using Sanford.Multimedia.Midi;
using Sanford.Multimedia.Midi.UI;
using System.Windows.Forms;
using System.IO;

// TODOs:
// -  Make channel strip color changes made during playback take immediate effect in the piano control
// -  Inspector - More detail on Other events. // -  Override instrument by track via Channel Strip.
// -  Function for extracting tracks from re-writing midi file data that have events for more than one channel in a single track, exploded
//    into tracks such that there is only one track per midi channel.  Most Midis download seem to have one midi channel per track.  Perhaps
//    just detect on load if there are notes for more than one midi channel in a track and prompt to convert to a channel per track.

namespace MidiMix
{
    public partial class Form1 : Form
    {
        private AboutBox1 aboutBox = new AboutBox1();
        private MidiFileInspector inspector = new MidiFileInspector();
        private OutputDeviceDialog odd = new OutputDeviceDialog();
        private OutputDevice outDevice = null;
        private bool closing = false;
        private int outDeviceID = 0;
        string fileName = "";
        long fileSize = 0;
        private bool playing = false;

        public PianoControl Piano
        {
            get { return pianoControl1; }
        }
        public Sequence Sequence { get { return sequence; } }
        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            if (OutputDevice.DeviceCount == 0)
            {
                MessageBox.Show("No MIDI output devices available.", "Error!",
                    MessageBoxButtons.OK, MessageBoxIcon.Stop);

                Close();
            }
            else
            {
                try
                {
                    channelStripCollection.Enabled = false;
                    saveToolStripMenuItem.Enabled = false;
                    saveAsToolStripMenuItem.Enabled = false;
                    inspectorToolStripMenuItem.Enabled = false;
                    panelControls.Enabled = false;

                    outDevice = new OutputDevice(outDeviceID);
                    sequencer.InitializeSequence(); // hook up the delegates to the dispatcher
                    // sequence1.LoadProgressChanged += HandleLoadProgressChanged;
                    sequence.LoadCompleted += HandleLoadCompleted;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message, "Error!",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);

                    Close();
                }
            }
            base.OnLoad(e);
        }

        private void openFile()
        {
            try
            {
                sequencer.Stop();
                playing = false;
                sequence.LoadAsync(fileName);
                this.Cursor = Cursors.WaitCursor;
                buttonPlay.Enabled = false;
                buttonStop.Enabled = false;
                openToolStripMenuItem.Enabled = false;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        }

        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                fileName = openFileDialog.FileName;
                openFile();
            }
        }


        private void HandleLoadCompleted(object sender, AsyncCompletedEventArgs e)
        {
            if (e.Error == null)
            {
                this.Cursor = Cursors.Arrow;
                buttonPlay.Enabled = true;
                buttonStop.Enabled = true;
                openToolStripMenuItem.Enabled = true;


                // Now enable the Save and Save As.. menu items.
                saveToolStripMenuItem.Enabled = true;
                saveAsToolStripMenuItem.Enabled = true;
                inspectorToolStripMenuItem.Enabled = true;
                panelControls.Enabled = true;
                channelStripCollection.initControls(sequence);
                channelStripCollection.Enabled = true;
                markersControl1.Enabled = false;
                checkBoxLoopMarked.Checked = false;

                // save the filesize
                FileInfo f = new FileInfo(fileName);
                    fileSize = f.Length;

                buildPresetsMenu();
                sequencer.Position = 0;
            }
            else
            {
                MessageBox.Show(e.Error.Message);
            }
        }

        private void buildPresetsMenu()
        {
            presetsToolStripMenuItem.DropDownItems.Clear();
            if (channelStripCollection.Controls.Count == 0)
            {
                presetsToolStripMenuItem.Enabled=false;
                return;
            }

            ToolStripMenuItem normalItem = new ToolStripMenuItem("Normal");
            normalItem.Click += new EventHandler(presetMenuItem_Click);
            presetsToolStripMenuItem.DropDownItems.Add(normalItem);

            Dictionary<int, ChannelStripSettings>.Enumerator e = channelStripCollection.ChannelStripSettingsEnumerator;
            while (e.MoveNext())
            {
                ChannelStripSettings css = e.Current.Value;
                ToolStripMenuItem trackMenuItem = new ToolStripMenuItem("Track " + css.TrackNum + ": " + css.TrackName);
                
                ToolStripMenuItem soloItem = new ToolStripMenuItem("Solo");
                ToolStripMenuItem highlightItem = new ToolStripMenuItem("Highlight");
                ToolStripMenuItem challengeItem = new ToolStripMenuItem("Challenge");
                soloItem.Tag = highlightItem.Tag = challengeItem.Tag = css;

                soloItem.Click += new EventHandler(presetMenuItem_Click);
                highlightItem.Click += new EventHandler(presetMenuItem_Click);
                challengeItem.Click += new EventHandler(presetMenuItem_Click);

                trackMenuItem.DropDownItems.Add(soloItem);
                trackMenuItem.DropDownItems.Add(highlightItem);
                trackMenuItem.DropDownItems.Add(challengeItem);
                
                presetsToolStripMenuItem.DropDownItems.Add(trackMenuItem);

            }
            presetsToolStripMenuItem.Enabled = true;
        }

        private void presetMenuItem_Click(object sender, EventArgs e) {
            ToolStripMenuItem item = (ToolStripMenuItem)sender;
            
            ChannelStripSettings selectedCSS=null;
            if (item.Tag != null && item.Tag.GetType() == typeof (ChannelStripSettings))
                selectedCSS = (ChannelStripSettings)item.Tag;

            Dictionary<int, ChannelStripSettings>.Enumerator en = channelStripCollection.ChannelStripSettingsEnumerator;
            while (en.MoveNext())
            {
                ChannelStripSettings currentCSS = en.Current.Value;
                currentCSS.Mute = false;
                currentCSS.Solo = false;
                currentCSS.ShowOnPiano = true;
                currentCSS.ValuePercent = 0.75F;
                switch (item.Text)
                {
                    case "Solo":
                        currentCSS.Solo = (currentCSS == selectedCSS);
                        currentCSS.ShowOnPiano = (currentCSS == selectedCSS);
                        break;
                    case "Highlight":
                        currentCSS.ValuePercent = (currentCSS == selectedCSS) ? 0.9F : 0.3F;
                        break;
                    case "Challenge":
                        currentCSS.Mute = (currentCSS == selectedCSS);
                        break;
                    case "Normal":
                        // do nothing
                        break;

                }
            }
            channelStripCollection.UpdateControls();
        }

         private void buttonPlay_Click(object sender, EventArgs e)
        {
            if (playing) // i.e. pause button clicked.
            {
                sequencer.Stop();
                buttonPlay.Text = "Play";
                playing = false;
                timer1.Stop();
            }
            else
            {
                try
                {
                    pianoControl1.BrushArray.Clear();
                    for (int i = 0; i < sequence.Count; i++)
                    {
                        ChannelStripSettings css = channelStripCollection.GetChannelStripSettingsForTrack(i);
                        pianoControl1.BrushArray.Insert(i, (css == null) ? new SolidBrush(Color.SkyBlue) : new SolidBrush(css.TrackColor));
                    }
                    playing = true;
                    buttonPlay.Text = "Pause";
                    buttonStop.Enabled = true;
                    if (sequencer.Position == 0)
                        sequencer.Start();
                    else
                        sequencer.Continue();
                    timer1.Start();
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                }
            }
        }
        private void buttonStop_Click(object sender, EventArgs e)
        {
            if (!playing)
            {
                int newPosition = 0;
                double percentComplete = (double)trackPlayhead.Value / (double)trackPlayhead.Maximum;

                if (checkBoxLoopMarked.Checked && percentComplete > markersControl1.InLoc / 100D)
                {
                    newPosition = (int)((markersControl1.InLoc / 100D) * (trackPlayhead.Maximum - trackPlayhead.Minimum));
                }
                trackPlayhead.Value =sequencer.Position = newPosition;
                
            }
            else
            {
                try
                {
                    buttonPlay.Text = "Play";
                    playing = false;
                    sequencer.Stop();
                    timer1.Stop();
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                }
            }

        }

        private void selectOutputDeviceToolStripMenuItem_Click(object sender, EventArgs e)
        {
            DialogResult result = odd.ShowDialog();
            if (result == DialogResult.OK)
            {
                outDeviceID = odd.OutputDeviceID;
            }
            else
            {
                Console.WriteLine("Problem with OutputDeviceDialog: " + result);
            }

        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            sequence.Dispose();

            if (outDevice != null)
            {
                outDevice.Dispose();
            }

            odd.Dispose();

            base.OnClosed(e);
        }

        private void HandleChased(object sender, ChasedEventArgs e)
        {
            foreach (ChannelMessage message in e.Messages)
            {
                outDevice.Send(message);
            }
        }
        private void HandleChannelMessagePlayed(object sender, ChannelMessageEventArgs e)
        {
            if (closing)
            {
                return;
            }

            int trackIndex = -1;

            for (int i = 0; i < sequence.Count; i++)
            {
                if (e.Track == sequence[i])
                    trackIndex = i;
            }

            ChannelStripSettings css=null;
            if (trackIndex >= 0)
                css = channelStripCollection.GetChannelStripSettingsForTrack(trackIndex);

            if (css != null)
            {
                // Replace the message with one based on processing the event through the channelStrip 
                ChannelMessage cm = new ChannelMessage(e.Message.Command, e.Message.MidiChannel, e.Message.Data1, (int)(Math.Round(e.Message.Data2 * css.ValuePercent)));

                // Note Off events are often encoded as noteOn with velocity zero.
                if (cm.Command == ChannelCommand.NoteOn && cm.Data2 > 0)
                {
                    if (!css.Mute)
                    {
                        if (channelStripCollection.SoloCount == 0 || (css.Solo))
                        {
                            outDevice.Send(cm);
                        }
                    }
                    if (css.ShowOnPiano)
                        pianoControl1.Send(e.Message, trackIndex);
                }
                else // always send note off messages (in case ShowOnPiano or Mute got toggled after the noteOn was sent)
                {
                    // TODO: be smarter about this.   Can track if a note is playing for this track/noteID
                    // and when muted / not shown, only send if the noteOff is needed to cancel a playing note.
                    // That would stop noteoff events for a unshown track turning off held ones on the same 
                    // noteId for another track.  Relatively minor thing.
                    // 
                    outDevice.Send(e.Message);
                    pianoControl1.Send(e.Message, (css.ShowOnPiano && !css.Mute) ? trackIndex : -1);
                }
            }
            else // no channel strip for this track - just play it - should not happen
            {
                outDevice.Send(e.Message);
                pianoControl1.Send(e.Message, -1);
            }

            double percentComplete = (double)sequencer.Position / (double)Sequence.GetLength();
            if (checkBoxLoopMarked.Checked && percentComplete >= (markersControl1.OutLoc / 100D))
            {
                sequencer.Stop();
                processPlayingCompleted();
            }

        }

        private void HandleSysExMessagePlayed(object sender, SysExMessageEventArgs e)
        {
            //     outDevice.Send(e.Message); Sometimes causes an exception to be thrown because the output device is overloaded.
        }

        private void HandleStopped(object sender, StoppedEventArgs e)
        {
            foreach (ChannelMessage message in e.Messages)
            {
                outDevice.Send(message);
                pianoControl1.Send(message,-1);
            }
            pianoControl1.ReleaseAllPianoKeys();
        }

        private void processPlayingCompleted() // responds to teh event from teh sequencer to say its done.
        {
            if (InvokeRequired)
            {
                BeginInvoke(new MethodInvoker(processPlayingCompleted));
                return;
            }

            buttonPlay.Text = "Play";
            playing = false;
            timer1.Stop();
            if (checkBoxLoopMarked.Checked)
            {
                playFromInMarker();
            }
        }

        private void HandlePlayingCompleted(object sender, EventArgs e)
        {
            processPlayingCompleted(); 
        }

        private void playFromInMarker()
        {
            if (InvokeRequired)
            {
                BeginInvoke(new MethodInvoker(playFromInMarker));
                return;
            }

            sequencer.Position = (int)((markersControl1.InLoc / 100D) * (double)sequence.GetLength());
            trackPlayhead.Value =
                trackPlayhead.Minimum +
                (int)((markersControl1.InLoc / 100D) * (trackPlayhead.Maximum - trackPlayhead.Minimum));
            buttonPlay_Click(null, null);

        }
        private void pianoControl1_PianoKeyDown(object sender, PianoKeyEventArgs e)
        {
            #region Guard

            if (playing)
            {
                return;
            }

            #endregion

            outDevice.Send(new ChannelMessage(ChannelCommand.NoteOn, 0, e.NoteID, 127));
        }

        private void pianoControl1_PianoKeyUp(object sender, PianoKeyEventArgs e)
        {
            #region Guard

            if (playing)
            {
                return;
            }

            #endregion

            outDevice.Send(new ChannelMessage(ChannelCommand.NoteOff, 0, e.NoteID, 0));
        }

        private void saveToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (MessageBox.Show("This will replace the file you opened.  If you want to save a copy to a different file, select Save As... from the menu.  Are you sure you want to proceed?","Confirm Save", MessageBoxButtons.YesNo) == DialogResult.Yes)
            {
                SaveProcessedSequence(fileName);
            }
        }

        private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (DialogResult.OK == saveFileDialog.ShowDialog()) {
                fileName = saveFileDialog.FileName;
                SaveProcessedSequence(fileName);
            }
        }
        private void SaveProcessedSequence(string filename)
        {
            Sequence newSequence = new Sequence();

            // take a deep copy of the active sequence
            byte[] buffer = new byte[fileSize * 2]; // more than enough
            MemoryStream stream = new MemoryStream(buffer, true);
            using (stream)
            {
                sequence.Save(stream);
                stream.Position = 0;
                newSequence.Load(stream);
            }
            processSequence(newSequence);
            newSequence.Save(fileName);
        }
        private void processSequence(Sequence sequenceToProcess)
        {
            ChannelMessageBuilder cmb = new ChannelMessageBuilder();
            for (int i = 0; i < sequence.Count; i++)
            {
                Track t = sequenceToProcess[i];
                ChannelStripSettings css = channelStripCollection.GetChannelStripSettingsForTrack(i);
                if (css != null)
                {
                    for (int j = 0; j < t.Count; j++)
                    {
                        MidiEvent me = t.GetMidiEvent(j);
                        if (me.MidiMessage.MessageType == MessageType.Channel)
                        {
                            ChannelMessage cm = (ChannelMessage)me.MidiMessage;
                            cmb.Initialize(cm);

                            if (css.Mute)
                                cmb.Data2 = 0;
                            else
                                cmb.Data2 = (int)(cm.Data2 * css.ValuePercent);

                            cmb.Build();
                            ChannelMessage newMessage = cmb.Result;

                            // replace the MidiEvent in the track
                            int ticks = me.AbsoluteTicks;
                            t.RemoveAt(j);
                            t.Insert(ticks, newMessage);
                        }
                    }
                }
            }
        }

        private void aboutMidiMixToolStripMenuItem_Click(object sender, EventArgs e)
        {
            aboutBox.ShowDialog();
        }

        private void inspectorToolStripMenuItem_Click(object sender, EventArgs e)
        {
            inspector.Sequence = sequence;
            inspector.ShowDialog();
        }

        private void buttonResetSpeed_Click(object sender, EventArgs e)
        {
            trackSpeed.Value = 50;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            trackPlayhead.Value = (int)(Math.Floor(1000 * ((float)sequencer.Position / (float)sequence.GetLength())));
        }

        private void trackSpeed_ValueChanged(object sender, EventArgs e)
        {
            // Using a tan(x) curve on this so that speed change ramps the further you get away from 1x.
            // The trackbar itself runs from 0 to 100, with 1x at 50.
            // We need to shift that to a -1.26 to +1.26 range for x (50 mapping to 0)
            // We will then get tan(x) that will give us a y value ranging from about -3 to +3.
            // For 1x (value=50, x=0), y will be 0.
            // We will then use y value as an exponent to a constant to yield the tempo multiplier.
            // Regardless of choice of constant, the constanst raised to the 0th power = 1.0.  Thus 1x is 1x.
            // The constant just controls how extreme the range is.
            // I have chosen the constant so that the calculated multiplier ranges from about 0.28 to 3.5.

            double x = -1.26F + (1.26F - -1.26F) * ((double)trackSpeed.Value / (double)trackSpeed.Maximum);
            double y = Math.Tan(x);
            const double theConstant = 1.5F;
            sequencer.clock.SpeedMultiplier = Math.Pow(theConstant, y);
        }

        private void trackPlayhead_ValueChanged(object sender, EventArgs e)
        {
            // when playing the value is being mutated by timer driven events #
            // setting it to the current sequencer.Position
            
            if (!playing) 
            {
                sequencer.Position = (int)((double)sequence.GetLength() * (double)trackPlayhead.Value / (double)trackPlayhead.Maximum);
            }
        }

        private void checkBoxLoopMarked_CheckedChanged(object sender, EventArgs e)
        {
            markersControl1.Enabled = checkBoxLoopMarked.Checked;
        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void Form1_DragEnter(object sender, DragEventArgs e)
        {
            Array a = (Array)e.Data.GetData(DataFormats.FileDrop);

            if (a != null)
            {
                string s = a.GetValue(0).ToString();
                string ext = Path.GetExtension(s.ToUpper());
                if (ext.Equals(".MID"))
                    e.Effect = DragDropEffects.Copy;
                else
                    e.Effect = DragDropEffects.None;
            }
        }

        private void Form1_DragDrop(object sender, DragEventArgs e)
        {
            try
            {
                Array a = (Array)e.Data.GetData(DataFormats.FileDrop);

                if (a != null)
                {
                    // Extract string from first array element
                    // (ignore all files except first if number of files are dropped).
                    string s = a.GetValue(0).ToString();
                    string ext = Path.GetExtension(s.ToUpper());
                    if (ext.Equals(".MID"))
                    {
                        fileName = s;
                        this.BeginInvoke(new MethodInvoker(openFile));
                        this.Activate();        // in the case Explorer overlaps this form
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in DragDrop function: " + ex.Message);
            }
        }
    }
}
